// Copyright (c) 2020 Presto Labs Pte. Ltd.
// Author: taekwon

#pragma once

#include <any>
#include <ctime>
#include <list>
#include <map>
#include <sstream>
#include <string>

#include <rapidjson/stringbuffer.h>
#include <rapidjson/writer.h>

class UrlParamsBuilder {
 public:
  std::string ComputeQueryString() const {
    std::string address;
    for (const auto& p : get_map_) {
      if (address.empty()) {
        address = p.first + "=" + p.second;
      } else {
        address = address + "&" + p.first + "=" + p.second;
      }
    }
    return address;
  }

  std::string ComputePostBodyAsQueryString() const {
    std::string address;
    for (const auto& p : post_map_) {
      std::string str;

      if (p.second.type() == typeid(std::string)) {
        str = std::any_cast<std::string>(p.second);
      } else if (p.second.type() == typeid(double)) {
        str = std::to_string(std::any_cast<double>(p.second));
      } else if (p.second.type() == typeid(int64_t)) {
        str = std::to_string(std::any_cast<int64_t>(p.second));
      } else if (p.second.type() == typeid(bool)) {
        str = std::any_cast<bool>(p.second) ? "true" : "false";
      } else if (p.second.type() == typeid(std::list<std::string>)) {
        // ignore
        continue;
      } else {
        CHECK(false) << "Unsupport type:" << p.second.type().name();
      }

      if (address.empty()) {
        address = p.first + "=" + str;
      } else {
        address = address + "&" + p.first + "=" + str;
      }
    }

    return address;
  }

  std::string ComputePostBody() const {
    if (!is_post_) {
      return "";
    }

    rapidjson::StringBuffer str_buf;
    rapidjson::Writer<rapidjson::StringBuffer> writer(str_buf);
    writer.StartObject();

    for (const auto& p : post_map_) {
      writer.Key(p.first.c_str());

      if (p.second.type() == typeid(std::string)) {
        writer.String(std::any_cast<std::string>(p.second).c_str());
      } else if (p.second.type() == typeid(double)) {
        writer.Double(std::any_cast<double>(p.second));
      } else if (p.second.type() == typeid(int64_t)) {
        writer.Int64(std::any_cast<int64_t>(p.second));
      } else if (p.second.type() == typeid(bool)) {
        writer.Bool(std::any_cast<bool>(p.second));
      } else if (p.second.type() == typeid(std::list<std::string>)) {
        const auto& list = std::any_cast<std::list<std::string>>(p.second);
        writer.StartArray();
        for (const auto& str : list) {
          writer.String(str.c_str());
        }
        writer.EndArray();
      } else {
        CHECK(false) << "Unsupport type:" << p.second.type().name();
      }
    }

    writer.EndObject();
    return str_buf.GetString();
  }

  UrlParamsBuilder& PutPost(const std::string& pre, const std::string& lparam) {
    if (lparam.empty()) {
      return *this;
    }

    if (!is_post_) {
      is_post_ = true;
    }

    post_map_[pre] = lparam;

    return *this;
  }

  template <
      bool is_raw = false,
      typename T,
      typename = std::enable_if_t<!std::is_convertible<T, std::string>::value>>
  UrlParamsBuilder& PutPost(const std::string& pre, const T& lparam) {
    if (!is_post_) {
      is_post_ = true;
    }

    if constexpr (is_raw) {
      post_map_[pre] = lparam;
    } else {
      PutPostImpl(pre, lparam);
    }

    return *this;
  }

  UrlParamsBuilder& PutGet(const std::string& pre, const std::string& lparam) {
    if (lparam.empty()) {
      return *this;
    }
    get_map_[pre] = lparam;
    return *this;
  }

  template <typename T, typename = std::enable_if_t<!std::is_convertible<T, std::string>::value>>
  UrlParamsBuilder& PutGet(const std::string& pre, const T& lparam) {
    return PutGetImpl(pre, lparam);
  }

  void SetIsPost(bool is_post) { is_post_ = is_post; }

  auto GetPostMap() const -> const std::map<std::string, std::any>& { return post_map_; }

 private:
  template <typename T>
  UrlParamsBuilder& PutGetImpl(const std::string& pre, T lparam) {
    std::stringstream ss;
    ss << lparam;
    get_map_[pre] = ss.str();
    return *this;
  }

  template <typename T>
  UrlParamsBuilder& PutPostImpl(const std::string& pre, T lparam) {
    std::stringstream ss;
    ss << lparam;
    post_map_[pre] = ss.str();
    return *this;
  }

 private:
  bool is_post_ = false;
  std::map<std::string, std::any> post_map_;
  std::map<std::string, std::string> get_map_;
};
